package com.chen.cloudpicture.manager.auth;

import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.json.JSONUtil;
import com.chen.cloudpicture.exception.BusinessException;
import com.chen.cloudpicture.exception.ErrorCode;
import com.chen.cloudpicture.manager.auth.model.SpaceUserPermissionConstant;
import com.chen.cloudpicture.model.entity.Picture;
import com.chen.cloudpicture.model.entity.Space;
import com.chen.cloudpicture.model.entity.SpaceUser;
import com.chen.cloudpicture.model.entity.User;
import com.chen.cloudpicture.model.enums.SpaceRoleEnum;
import com.chen.cloudpicture.model.enums.SpaceTypeEnum;
import com.chen.cloudpicture.service.PictureService;
import com.chen.cloudpicture.service.SpaceService;
import com.chen.cloudpicture.service.SpaceUserService;
import com.chen.cloudpicture.service.UserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.*;

import static com.chen.cloudpicture.constant.UserConstant.USER_LOGIN_STATE;

/**
 * 自定义权限加载接口实现类
 * 核心的权限认证类。在该类中实现 “根据登录用户 id 获取到用户已有的角色和权限列表” 方法。
 * 当要鉴权时 Sa-token首先执行我们编写的这个方法得到用户角色权限列表再进行比对鉴权。
 */
@Component    // 保证此类被 SpringBoot 扫描，完成 Sa-Token 的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {

    // 从配置文件获取url请求路径前缀，默认是 /api
    @Value("${server.servlet.context-path}")
    private String contextPath;
    @Resource
    private UserService userService;
    @Resource
    private PictureService pictureService;
    @Resource
    private SpaceService spaceService;
    @Resource
    private SpaceUserService spaceUserService;
    @Resource
    private SpaceUserAuthManager spaceUserAuthManager;

    /**
     * 返回一个账号所拥有的权限码集合
     * 一套通用的权限校验逻辑，兼容公共图库、私有空间和团队空间的权限 （没什么新东西，就是业务判断流程）
     * 流程：
     * 1. 从上下文中获取 SpaceUserAuthContext 上下文对象，管理员直接获取全部权限
     * 2. 从 SpaceUserAuthContext 对象中获取 SpaceUser(团队空间对象)、SpaceUserId(团队空间表id)、SpaceId、PictureId
     * 3. 校验 SpaceUserAuthContext 对象的字段是否为空
     * 4. 如果所有字段都为空，表示查询公共图库，可以通过
     * 5. 如果有 SpaceUser 对象，直接返回 SpaceUser 对象的权限列表
     * 6. 如果有 SpaceUserId 对象，通过数据库查询 SpaceUser 对象，返回 SpaceUser 对象的权限列表
     * 7. 如果有 SpaceId 对象，通过数据库查询 Space 对象，返回 Space 对象的权限列表
     * 8. 如果有 PictureId 对象，通过数据库查询 Picture 对象，再通过 Picture 查询对应的 space 空间，返回 space 空间的权限列表
     */
    /**
     * 这么写下来发现编写一套统一的权限校验逻辑并不容易，所以实际项目中要 按需使用 第三方权限校验框架。注解 + AOP切面就挺好的，
     * 或者编程式鉴权，从ThreadLocal获取上下文也更加灵活高效。这里注解式获取参数判断权限逻辑时，可能会重复查询数据库，对性能造成影响
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 判断 loginType，仅对类型为 "space" 进行权限校验
        if (!StpKit.SPACE_TYPE.equals(loginType)) {
            return new ArrayList<>();
        }
        // 管理员权限，表示权限校验通过
        List<String> ADMIN_PERMISSIONS = spaceUserAuthManager.getPermissionsByRole(SpaceRoleEnum.ADMIN.getValue());
        // 获取上下文对象
        SpaceUserAuthContext authContext = getAuthContextByRequest();
        // 如果所有字段都为空，表示查询公共图库，可以通过
        if (isAllFieldsNull(authContext)) {
            return ADMIN_PERMISSIONS;
        }
        // 获取 userId
        User loginUser = (User) StpKit.SPACE.getSessionByLoginId(loginId).get(USER_LOGIN_STATE);
        if (loginUser == null) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "用户未登录");
        }
        Long userId = loginUser.getId();
        // 优先从上下文中获取 SpaceUser 对象
        SpaceUser spaceUser = authContext.getSpaceUser();
        if (spaceUser != null) {
            return spaceUserAuthManager.getPermissionsByRole(spaceUser.getSpaceRole());
        }
        // 如果有 spaceUserId，必然是团队空间，通过数据库查询 SpaceUser 对象
        Long spaceUserId = authContext.getSpaceUserId();
        if (spaceUserId != null) {
            spaceUser = spaceUserService.getById(spaceUserId);
            if (spaceUser == null) {
                throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到空间用户信息");
            }
            // 取出当前登录用户对应的 spaceUser
            SpaceUser loginSpaceUser = spaceUserService.lambdaQuery()
                    .eq(SpaceUser::getSpaceId, spaceUser.getSpaceId())
                    .eq(SpaceUser::getUserId, userId)
                    .one();
            if (loginSpaceUser == null) {
                return new ArrayList<>();
            }
            // 这里会导致管理员在私有空间没有权限，可以再查一次库处理
            return spaceUserAuthManager.getPermissionsByRole(loginSpaceUser.getSpaceRole());
        }
        // 如果没有 spaceUserId，尝试通过 spaceId 或 pictureId 获取 Space 对象并处理
        Long spaceId = authContext.getSpaceId();
        if (spaceId == null) {
            // 如果没有 spaceId，通过 pictureId 获取 Picture 对象和 Space 对象
            Long pictureId = authContext.getPictureId();
            // 图片 id 也没有，则默认通过权限校验
            if (pictureId == null) {
                return ADMIN_PERMISSIONS;
            }
            Picture picture = pictureService.lambdaQuery()
                    .eq(Picture::getId, pictureId)
                    .select(Picture::getId, Picture::getSpaceId, Picture::getUserId)
                    .one();
            if (picture == null) {
                throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到图片信息");
            }
            spaceId = picture.getSpaceId();
            // 公共图库，仅本人或管理员可操作
            if (spaceId == null) {
                if (picture.getUserId().equals(userId) || userService.isAdmin(loginUser)) {
                    return ADMIN_PERMISSIONS;
                } else {
                    // 不是自己的图片，仅可查看
                    return Collections.singletonList(SpaceUserPermissionConstant.PICTURE_VIEW);
                }
            }
        }
        // 获取 Space 对象
        Space space = spaceService.getById(spaceId);
        if (space == null) {
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到空间信息");
        }
        // 根据 Space 类型判断权限
        if (space.getSpaceType() == SpaceTypeEnum.PRIVATE.getValue()) {
            // 私有空间，仅本人或管理员有权限
            if (space.getUserId().equals(userId) || userService.isAdmin(loginUser)) {
                return ADMIN_PERMISSIONS;
            } else {
                return new ArrayList<>();
            }
        } else {
            // 团队空间，查询 SpaceUser 并获取角色和权限
            spaceUser = spaceUserService.lambdaQuery()
                    .eq(SpaceUser::getSpaceId, spaceId)
                    .eq(SpaceUser::getUserId, userId)
                    .one();
            if (spaceUser == null) {
                return new ArrayList<>();
            }
            return spaceUserAuthManager.getPermissionsByRole(spaceUser.getSpaceRole());
        }
    }

    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     * (本项目中未使用)
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        return new ArrayList<>();
    }

    /**
     * 从请求中获取上下文对象
     */
    private SpaceUserAuthContext getAuthContextByRequest(){
        // 获取请求对象
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        // 获取请求类型（GET/POST,我们编写的所有接口都是 GET 或 POST 类型）
        String contentType = request.getHeader(Header.CONTENT_TYPE.getValue());
        SpaceUserAuthContext authContext;
        // 获取请求参数
        if(ContentType.JSON.getValue().equals(contentType)){
            // 如果是 POST 请求，直接从请求体的 JSON 对象获得
            /**
             * ServletUtil.getBody(request); 这段代码有个地方非常坑，
             * HttpServletRequest 的 body 值是个流，流只支持读取一次，读完就没了，在别的地方还想再读取 body 就会报错。
             * 所以为了解决这个问题，我们还要在 config 包下自定义请求包装类和请求包装类过滤器。
             */
            String body = ServletUtil.getBody(request);
            authContext = JSONUtil.toBean(body, SpaceUserAuthContext.class);
        } else {
            // 如果是 GET 请求，从包含所有请求参数的 map 集合获得
            Map<String, String> paramMap = ServletUtil.getParamMap(request);
            authContext = BeanUtil.toBean(paramMap, SpaceUserAuthContext.class);
        }
        // 根据请求路径区分 id 字段的含义
        Long id = authContext.getId();
        if(ObjUtil.isNotNull(id)){
            // 获取 url 请求路径的业务前缀   /api/picture/aaa?a=1 ，去掉/api/
            String requestURI = request.getRequestURI();
            String partURI = requestURI.replace(contextPath + "/", "");
            // 截取第一个斜杠前的字符串就是业务前缀
            String moduleName = StrUtil.subBefore(partURI, "/", false);
            switch (moduleName) {
                case "picture":
                    authContext.setPictureId(id);
                    break;
                case "spaceUser":
                    authContext.setSpaceUserId(id);
                    break;
                case "space":
                    authContext.setSpaceId(id);
                    break;
                default:
            }
        }
        return authContext;
    }

    private boolean isAllFieldsNull(Object object) {
        if (object == null) {
            return true; // 对象本身为空
        }
        // 反射获取所有字段并判断是否所有字段都为空
        return Arrays.stream(ReflectUtil.getFields(object.getClass()))
                // 获取字段值
                .map(field -> ReflectUtil.getFieldValue(object, field))
                // 检查是否所有字段都为空
                .allMatch(ObjectUtil::isEmpty);
    }

}
